<?php
/**
 * Created by PhpStorm.
 * User: Qch
 * Date: 2018/9/9
 * Time: 7:37
 */

namespace J\Util;


use J\Message;

/**
 * HTTP 单文件上传
 *   多文件应该由客户端多次发起
 *
 *    (new UploadFile())->rule()->save()
 *
 * <form enctype="multipart/form-data" action="__URL__" method="POST">
 *    <!-- MAX_FILE_SIZE must precede the file input field -->
 *    <input type="hidden" name="MAX_FILE_SIZE" value="30000" />
 *    <!-- Name of input element determines name in $_FILES array -->
 *    Send this file: <input name="file" type="file" />
 *    <input type="submit" value="Send File" />
 * </form>
 *
 * PHP文件上传，php.ini中的配置项为：
 *      file_uploads (ON)  PHP脚本是否可以接受HTTP文件上传
 *      upload_max_filesize (2M)  限制PHP处理上传文件大小的最大值，此值必须小于post_max_size值
 *      post_max_size (8M)  POST方法接受信息的最大值
 *                                 除了上传文件，可能有其他的表单域，所以要大于upload_max_filesize
 *      upload_tmp_dir (NULL)  上传文件临时路径，对于拥有此服务器的进程用户必须是可写的。
 *                                默认为操作系统的临时文件夹，Linux的是在/tmp目录下
 *
 * 全局数组$_FILES，其中file为表单中name的值:
 *      $_FILES['file']['name'] 客户端机器文件的原名称。
 *      $_FILES['file']['type'] 文件的 MIME 类型，如果浏览器提供此信息的话。
 *                              一个例子是“image/gif”。
 *                             不过此 MIME 类型在 PHP 端并不检查，因此不要想当然认为有这个值。
 *      $_FILES['file']['size'] 已上传文件的大小，单位为字节。
 *      $_FILES['file']['tmp_name'] 文件被上传后在服务端储存的临时文件名。
 *      $_FILES['file']['error'] 和该文件上传相关的错误代码。
 *                UPLOAD_ERR_OK         其值为 0，没有错误发生，文件上传成功。
 *                UPLOAD_ERR_INI_SIZE   其值为 1，上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值。
 *                UPLOAD_ERR_FORM_SIZE  其值为 2，上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值。
 *                UPLOAD_ERR_PARTIAL    其值为 3，文件只有部分被上传。
 *                UPLOAD_ERR_NO_FILE    其值为 4，没有文件被上传。
 *                UPLOAD_ERR_NO_TMP_DIR 其值为 6，找不到临时文件夹。PHP 4.3.10 和 PHP 5.0.3 引进。
 *                UPLOAD_ERR_CANT_WRITE 其值为 7，文件写入失败。PHP 5.1.0 引进。
 * 两个函数：
 *      bool is_uploaded_file(string filename)。  用于判断是否为HTTP POST上传的文件，如果是则返回true。
 *          参数必须类似$_FILES['file']['tmp_name']，不能是$_FILES['file']['name']，
 *          用于防止潜在的攻击者对原本不能通过脚本交互的文件进行非法管理。
 *      bool move_uploaded_file(string filename,string destination)。
 *          将HTTP POST上传的临时文件移动到新的路径。成功返回TRUE
 *
 * 上传文件流程：
 *
 *      通过$_FILES['file']['error']判断是否有错误产生，有错误则提示并退出脚本。
 *      通过$_FILES['file']['type']判断是否为允许的上传类型，如果不在允许上传的类型中，提示并退出脚本。
 *      把$_FILES['file']['name']分割为数组，获取文件后缀名，
 *      通过$_FILES['file']['size']判断是否超出了PHP限制的大小。
 *      通过is_uploaded_file()函数和move_uploaded_file()将文件上传到相应的目录中。
 */
class UploadFile
{
    /**
     * @var string 错误信息
     */
    private $error = '';

    /**
     * @var string 上传文件名
     */
    protected $file;

    /**
     * @var string 本地文件名
     */
    protected $name;

    /**
     * @var array 校验规则
     */
    protected $_rule = null;

    /**
     * @var array 上传文件信息
     */
    protected $info;

    /**
     * @var array 文件 hash 信息
     */
    protected $hash = [];

    /**
     * 设定校验规则
     * @param array $rule
     * @return static
     */
    public function rule($rule)
    {
        $this->_rule = $rule;

        return $this;
    }

    /**
     * 尝试保存文件
     * @param string $path 保存路径
     * @param string $key form-field-name
     * @return boolean
     */
    public function save($path, $key = 'file')
    {
        // 无上传
        if (empty($_FILES)) return null;
        // 无数据
        if (!isset($_FILES[$key]) or empty($_FILES[$key])) return null;

        $this->info = $_FILES[$key];

        // 文件上传失败，捕获错误代码
        if (!empty($this->info['error'])) {
            $this->error = static::error($this->info['error']);
            return false;
        }

        // 检测合法性
        if (!is_uploaded_file($this->info['tmp_name'])) {
            $this->error = Message::FILE_UPLOAD_POST;
            return false;
        }

        // 验证上传
        if (!$this->check()) return false;

        // 文件保存命名规则
        $path = rtrim($path, DS) . DS;
        $this->name = $this->buildSaveName();
        $filename = $path . $this->name;

        $dir = dirname($filename);

        if (!is_dir($dir) and !mkdir($dir, 0755, true)) {
            $this->error = Message::FILE_UPLOAD_PATH;
            return false;
        }


        if (!move_uploaded_file($this->info['tmp_name'], $filename)) {
            $this->error = Message::FILE_UPLOAD_WRITE;
            return false;
        }

        return true;
    }

    /**
     * 本地文件名
     * @return string
     */
    public function getName()
    {
        if (!$this->name) {
            $name = $this->buildSaveName();
            // 有效性检查
            if ('' === $name || false === $name) $name = $this->file;
            // 文件扩展名
            if (!strpos($name, '.')) $name .= '.' . pathinfo($this->file, PATHINFO_EXTENSION);

            $this->name = $name;
        }
        return $this->name;
    }

    /**
     * 获取文件的哈希散列值
     *
     * @param  string $type 类型
     * @return string
     */
    public function hash($type = 'sha1')
    {
        if (!isset($this->hash[$type])) {
            $this->hash[$type] = hash_file($type, $this->file);
        }

        return $this->hash[$type];
    }

    /**
     * 获取文件类型信息
     * @access public
     * @return string
     */
    public function getMime()
    {
        $info = finfo_open(FILEINFO_MIME_TYPE);

        return finfo_file($info, $this->file);
    }

    /**
     * 检测上传文件
     *
     * @return bool
     */
    protected function check()
    {
        $rule = $this->_rule;

        /* 检查文件大小 */
        if (isset($rule['size']) && !$this->checkSize($rule['size'])) {
            $this->error = Message::FILE_UPLOAD_SIZE;
            return false;
        }

        /* 检查文件 Mime 类型 */
        if (isset($rule['type']) && !$this->checkMime($rule['type'])) {
            $this->error = Message::FILE_UPLOAD_MIME;
            return false;
        }

        /* 检查文件后缀 */
        if (isset($rule['ext']) && !$this->checkExt($rule['ext'])) {
            $this->error = Message::FILE_UPLOAD_EXT;
            return false;
        }

        /* 检查图像文件 */
        if (!$this->checkImg()) {
            $this->error = Message::FILE_UPLOAD_IMG;
            return false;
        }

        return true;
    }

    /**
     * 检测上传文件后缀
     * @access public
     * @param  array|string $ext 允许后缀
     * @return bool
     */
    protected function checkExt($ext)
    {
        if (is_string($ext))
            $ext = explode(',', $ext);

        $extension = strtolower(pathinfo($this->file, PATHINFO_EXTENSION));

        return in_array($extension, $ext);
    }

    /**
     * 检测图像文件
     * @access public
     * @return bool
     */
    protected function checkImg()
    {
        $extension = strtolower(pathinfo($this->file, PATHINFO_EXTENSION));

        // 如果上传的不是图片，或者是图片而且后缀确实符合图片类型则返回 true
        return !in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) ||
            in_array($this->getImageType($this->file), [1, 2, 3, 4, 6, 13]);
    }

    /**
     * 判断图像类型
     *
     * @param  string $image 图片名称
     * @return bool|int
     */
    protected function getImageType($image)
    {
        if (function_exists('exif_imagetype'))
            return exif_imagetype($image);

        try {
            $info = getimagesize($image);
            return $info ? $info[2] : false;
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * 检测上传文件大小
     *
     * @param  integer $size 最大大小
     * @return bool
     */
    protected function checkSize($size)
    {
        return $this->info['size'] <= $size;
    }

    /**
     * 检测上传文件类型
     *
     * @param  array|string $mime 允许类型
     * @return bool
     */
    protected function checkMime($mime)
    {
        $mime = is_string($mime) ? explode(',', $mime) : $mime;

        return in_array(strtolower($this->getMime()), $mime);
    }

    /**
     * 获取保存文件名
     *
     * @return string
     */
    protected function buildSaveName()
    {
        $namer = array_isset($this->_rule, 'name', null);

        if ($namer instanceof \Closure)
            return ($namer)($this->file);
        if (is_callable($namer))
            return call_user_func_array($namer, [$this->file]);

        return date('Ymd') . DS . md5(microtime(true));
    }

    /**
     * 获取错误代码信息
     *
     * @param  int $errorNo 错误号
     * @return string
     */
    protected static function error($errorNo)
    {
        switch ($errorNo) {
            case 1:
            case 2:
                return 'upload File size exceeds the maximum value';
            case 3:
                return 'only the portion of file is uploaded';
            case 4:
                return 'no file to uploaded';
            case 6:
                return 'upload temp dir not found';
            case 7:
                return 'file write error';
                break;
            default:
                return 'unknown upload error';
        }
    }
}